diff options
Diffstat (limited to 'app/[lng]')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/bid/page.tsx | 111 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/bidding-notice/page.tsx | 33 |
2 files changed, 144 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/bid/page.tsx b/app/[lng]/evcp/(evcp)/bid/page.tsx new file mode 100644 index 00000000..7480ce88 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/bid/page.tsx @@ -0,0 +1,111 @@ +import { Suspense } from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { + getBiddings, + getBiddingStatusCounts, + getBiddingTypeCounts, + getBiddingManagerCounts, + getBiddingMonthlyStats +} from "@/lib/bidding/service" +import { searchParamsCache } from "@/lib/bidding/validation" +import { BiddingsPageHeader } from "@/lib/bidding/list/biddings-page-header" +import { BiddingsStatsCards } from "@/lib/bidding/list/biddings-stats-cards" +import { BiddingsTable } from "@/lib/bidding/list/biddings-table" + +export const metadata = { + title: "입찰 목록", + description: "입찰 공고를 생성하고 진행 상황을 관리할 수 있습니다.", +} + +interface BiddingsPageProps { + searchParams: Record<string, string | string[] | undefined> +} + +export default async function BiddingsPage({ searchParams }: BiddingsPageProps) { + // ✅ nuqs searchParamsCache로 파싱 (타입 안전성 보장) + const search = searchParamsCache.parse(searchParams) + + // ✅ 모든 데이터를 병렬로 로드 + const promises = Promise.all([ + getBiddings(search), + getBiddingStatusCounts(), + getBiddingTypeCounts(), + getBiddingManagerCounts(), + getBiddingMonthlyStats(), + ]) + + return ( + <Shell className="gap-4"> + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 페이지 헤더 */} + {/* ═══════════════════════════════════════════════════════════════ */} + <BiddingsPageHeader /> + + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 통계 카드들 */} + {/* ═══════════════════════════════════════════════════════════════ */} + <Suspense fallback={<BiddingsStatsCardsSkeleton />}> + <BiddingsStatsCardsWrapper promises={promises} /> + </Suspense> + + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 메인 테이블 */} + {/* ═══════════════════════════════════════════════════════════════ */} + <Suspense + fallback={ + <DataTableSkeleton + columnCount={20} + searchableColumnCount={3} + filterableColumnCount={4} + cellWidths={["10rem", "8rem", "12rem", "15rem", "10rem", "8rem"]} + shrinkZero + /> + } + > + <BiddingsTable promises={promises} /> + </Suspense> + </Shell> + ) +} + +// ═══════════════════════════════════════════════════════════════ +// 통계 카드 래퍼 컴포넌트 +// ═══════════════════════════════════════════════════════════════ +async function BiddingsStatsCardsWrapper({ + promises +}: { + promises: Promise<[ + Awaited<ReturnType<typeof getBiddings>>, + Awaited<ReturnType<typeof getBiddingStatusCounts>>, + Awaited<ReturnType<typeof getBiddingTypeCounts>>, + Awaited<ReturnType<typeof getBiddingManagerCounts>>, + Awaited<ReturnType<typeof getBiddingMonthlyStats>> + ]> +}) { + const [biddingsResult, statusCounts, typeCounts, managerCounts, monthlyStats] = await promises + + return ( + <BiddingsStatsCards + total={biddingsResult.total} + statusCounts={statusCounts} + typeCounts={typeCounts} + managerCounts={managerCounts} + monthlyStats={monthlyStats} + /> + ) +} + +// 통계 카드 스켈레톤 +function BiddingsStatsCardsSkeleton() { + return ( + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> + {Array.from({ length: 4 }).map((_, i) => ( + <div key={i} className="rounded-lg border p-6"> + <div className="h-4 bg-muted rounded animate-pulse mb-2" /> + <div className="h-8 bg-muted rounded animate-pulse" /> + </div> + ))} + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/bidding-notice/page.tsx b/app/[lng]/evcp/(evcp)/bidding-notice/page.tsx new file mode 100644 index 00000000..86e4bd6c --- /dev/null +++ b/app/[lng]/evcp/(evcp)/bidding-notice/page.tsx @@ -0,0 +1,33 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { BiddingNoticeEditor } from '@/lib/bidding/bidding-notice-editor' +import { getBiddingNoticeTemplate } from '@/lib/bidding/service' + +export default async function BiddingNoticePage() { + const template = await getBiddingNoticeTemplate() + + return ( + <div className="container mx-auto py-6 max-w-6xl"> + <div className="mb-6"> + <h1 className="text-3xl font-bold tracking-tight">입찰공고문 관리</h1> + <p className="text-muted-foreground mt-2"> + 표준 입찰공고문 템플릿을 작성하고 관리할 수 있습니다. + </p> + </div> + + <Card> + <CardHeader> + <CardTitle>표준 입찰공고문 템플릿</CardTitle> + <CardDescription> + 이 템플릿은 실제 입찰 공고 작성 시 기본 양식으로 사용됩니다. + 필요한 표준 정보와 서식을 미리 작성해두세요. + </CardDescription> + </CardHeader> + <CardContent> + <BiddingNoticeEditor + initialData={template} + /> + </CardContent> + </Card> + </div> + ) +}
\ No newline at end of file |
